package net.cyndeline.scalarlib.rldungeon.grammar.util

import scalax.collection.GraphEdge.UnDiEdge
import net.cyndeline.scalarlib.rldungeon.grammar.ComponentProduction
import scalax.collection.immutable.Graph
import net.cyndeline.scalarlib.rlgraph.util.{VertexReplacer, EdgeCopyFactory}
import net.cyndeline.scalarlib.rldungeon.dgs.graph.{DGSFunctionality, Room}
import scalax.collection.GraphPredef.OuterEdge
import net.cyndeline.scalarlib.util.EdgeFactory

/**
 * Generic ComponentProduction that modifies the topology of a graph. Supports:
 *
 *  # Adding vertices
 *  # Removing vertices (all connected edges are deleted)
 *  # Adding edges A ~ B where A and B are either a previous vertex, or a newly added vertex
 *  # Deleting edges A ~ B (A and B remains in the graph)
 *
 * Operations are performed in the following order: Vertex add, edge add, edge remove, vertex remove.
 */
class TopologyProduction[VType <: Room[VType], EType[X] <: UnDiEdge[X] with DGSFunctionality[X, EType] with OuterEdge[X, EType]] private
    (edgeCopyFactory: EdgeCopyFactory[VType, EType],
     edgeFactory: EdgeFactory[VType, EType],
     vertexFactory: VertexFactory[VType],
     vertexAdd: Set[Int],
     vertexRemove: Set[VType],
     vertexReplace: Set[VType],
     newEdgeAdd: Set[(Int, Int)],
     oldEdgeAdd: Set[(VType, VType)],
     mixEdgeAdd: Set[(Int, VType)],
     oldEdgeRemove: Set[(VType, VType)],
     newRoomModifications: Map[Int, VertexModification[VType]]) extends ComponentProduction[VType, EType] {

  private val vertexReplacer = new VertexReplacer[VType, EType](edgeCopyFactory)

  noOverlappingVerticesBetweenVertexRemovalAndEdgeAdditionAllowed()
  addingAndRemovingAnEdgeException()
  removingAndReplacingVertexException()
  modificationIdsExistException()

  /**
   * @constructor Creates an empty topology production.
   * @param edgeCopyFactory Used to copy edges when replacing vertices.
   * @param edgeFactory Produces new edges.
   * @param vertexFactory Creates new vertices.
   */
  def this(edgeCopyFactory: EdgeCopyFactory[VType, EType], edgeFactory: EdgeFactory[VType, EType], vertexFactory: VertexFactory[VType]) =
    this(edgeCopyFactory, edgeFactory, vertexFactory, Set(), Set(), Set(), Set(), Set(), Set(), Set(), Map())

  /**
   * Applies every topology modification specified in the constructor.
   *
   * @param morphism Maps a set of vertices in a production pattern to the vertices in the sub graph. It's up to the
   *                 ComponentProductions implementation to know which vertices to use as keys here.
   * @param totalGraph The graph that the sub graph appears in, in its entirety. Every vertex here is not guaranteed to
   *                   appear in the morphism. This graph is only supplied to allow ComponentProductions to perform
   *                   graph-spanning algorithms that doesn't rely on a particular pattern, as well as giving them the
   *                   graph as it appears before modification.
   *                   NOTE: Do not use this graph to check if the graph is valid for a particular modification, put
   *                   those checks in the negative condition inside the Production.
   * @return a graph modified by this production.
   */
  override def apply(morphism: Morphism[VType], totalGraph: Graph[VType, EType]): Graph[VType, EType] = {
    var addedVertices = Map[Int, VType]()
    var finalGraph = totalGraph

    val verticesToAdd = vertexAdd.iterator
    while (verticesToAdd.hasNext) {
      val vertexId: Int = verticesToAdd.next()
      val newVertex = if (newRoomModifications.contains(vertexId))
        newRoomModifications(vertexId).modify(vertexFactory.produce)
      else
        vertexFactory.produce
      addedVertices += (vertexId -> newVertex)

      finalGraph += newVertex
    }

    finalGraph = addNewEdges(newEdgeAdd, addedVertices, finalGraph)
    finalGraph = addOldEdges(oldEdgeAdd, morphism, finalGraph)
    finalGraph = addMixEdges(mixEdgeAdd, addedVertices, morphism, finalGraph)
    finalGraph = removeEdges(oldEdgeRemove, morphism, totalGraph, finalGraph)

    val verticesToRemove = vertexRemove.iterator
    while (verticesToRemove.hasNext) {
      val vertexInGraph = morphism.getVertexCorrespondingTo(verticesToRemove.next())
      finalGraph -= vertexInGraph
    }

    val verticesToReplace = vertexReplace.iterator
    while (verticesToReplace.hasNext) {
      val vertex: VType = verticesToReplace.next()
      val vertexToReplace = morphism.getVertexCorrespondingTo(vertex)
      val vertexToReplaceWith = vertexFactory.produce
      finalGraph = vertexReplacer.replace(vertexToReplace, vertexToReplaceWith, finalGraph)
    }

    finalGraph
  }

  /**
   * Creates a new vertex.
   * @param id Numeric identifier for the new vertex. This id will not be used in the graph, but is a way to reference
   *           the vertex being created when adding additional modifications (such as edge additions using the vertex).
   * @return a new TopologyProduction that adds an additional vertex to the input graph.
   */
  def addVertex(id: Int): TopologyProduction[VType, EType] = {
    if (!vertexAdd.contains(id))
      new TopologyProduction[VType, EType](edgeCopyFactory, edgeFactory, vertexFactory, vertexAdd + id, vertexRemove, vertexReplace, newEdgeAdd, oldEdgeAdd, mixEdgeAdd, oldEdgeRemove, newRoomModifications)
    else
      throw new IllegalArgumentException("The vertex id " + id + " was used twice.")
  }

  /**
   * Removes a vertex already present in the input graph.
   * @param v Vertex in the graph pattern representing the vertex to remove.
   * @return a new TopologyProduction that removes the vertex corresponding to the specified vertex in the morphism.
   */
  def removeVertex(v: VType): TopologyProduction[VType, EType] = {
    new TopologyProduction[VType, EType](edgeCopyFactory, edgeFactory, vertexFactory, vertexAdd, vertexRemove + v, vertexReplace, newEdgeAdd, oldEdgeAdd, mixEdgeAdd, oldEdgeRemove, newRoomModifications)
  }

  /**
   * Replaces a vertex already present in the graph with a new one. All edges going to the old vertex will be preserved
   * in the new one.
   * @param v Vertex to replace.
   * @return a new TopologyProduction that replaces the vertex corresponding to the specified vertex in the morphism.
   */
  def replaceVertex(v: VType) = {
    new TopologyProduction[VType, EType](edgeCopyFactory, edgeFactory, vertexFactory, vertexAdd, vertexRemove, vertexReplace + v, newEdgeAdd, oldEdgeAdd, mixEdgeAdd, oldEdgeRemove, newRoomModifications)
  }

  /**
   * Adds an edge between two vertices already present in the graph.
   * @param from A vertex in the graph.
   * @param to Another vertex in the graph.
   * @return a new TopologyProduction that adds an edge between the specified vertices in the morphism.
   */
  def addOldEdge(from: VType, to: VType): TopologyProduction[VType, EType] = {
    if (from != to)
      new TopologyProduction[VType, EType](edgeCopyFactory, edgeFactory, vertexFactory, vertexAdd, vertexRemove, vertexReplace, newEdgeAdd, oldEdgeAdd + ((from, to)), mixEdgeAdd, oldEdgeRemove, newRoomModifications)
    else
      throw new IllegalArgumentException("Cannot add edges to and from the same vertex (" + from + ")")
  }

  /**
   * Adds an edge between a previous vertex and a vertex added by this production.
   * @param from The id representing the newly added vertex.
   * @param to An old vertex in the graph morphism.
   * @return a new TopologyProduction that adds an edge between the specified vertices.
   */
  def addMixEdge(from: Int, to: VType): TopologyProduction[VType, EType] = {
    new TopologyProduction[VType, EType](edgeCopyFactory, edgeFactory, vertexFactory, vertexAdd, vertexRemove, vertexReplace, newEdgeAdd, oldEdgeAdd, mixEdgeAdd + ((from, to)), oldEdgeRemove, newRoomModifications)
  }

  /**
   * Adds an edge between two vertices added by this production.
   * @param from The id representing a newly added vertex.
   * @param to The id representing another newly added vertex. Must differ from the other vertex.
   * @return a new TopologyProduction that adds an edge between the specified vertices.
   */
  def addNewEdge(from: Int, to: Int): TopologyProduction[VType, EType] = {
    if (from != to)
      new TopologyProduction[VType, EType](edgeCopyFactory, edgeFactory, vertexFactory, vertexAdd, vertexRemove, vertexReplace, newEdgeAdd + ((from, to)), oldEdgeAdd, mixEdgeAdd, oldEdgeRemove, newRoomModifications)
    else
      throw new IllegalArgumentException("Cannot add edges to and from the same vertex (" + from + ")")
  }

  /**
   * Removes an edge already present in the graph.
   * @param from A vertex in the graph.
   * @param to Another vertex in the graph.
   * @return a new TopologyProduction that removes an edge between the specified vertices.
   */
  def removeEdge(from: VType, to: VType): TopologyProduction[VType, EType] = {
    new TopologyProduction[VType, EType](edgeCopyFactory, edgeFactory, vertexFactory, vertexAdd, vertexRemove, vertexReplace, newEdgeAdd, oldEdgeAdd, mixEdgeAdd, oldEdgeRemove + ((from, to)), newRoomModifications)
  }

  /**
   * Sets modification instructions for a new vertex to be created.
   * @param newVertexId Id of the created vertex (must already be in the production).
   * @param modification Modifications to apply to the vertex.
   * @return A new topology that modifies some of the vertices after creating it.
   */
  def addModification(newVertexId: Int, modification: VertexModification[VType]) = {
    new TopologyProduction[VType, EType](edgeCopyFactory, edgeFactory, vertexFactory, vertexAdd, vertexRemove, vertexReplace, newEdgeAdd, oldEdgeAdd, mixEdgeAdd, oldEdgeRemove, newRoomModifications + (newVertexId -> modification))
  }

  private def addNewEdges(newEdgeAdd: Set[(Int, Int)],
                          addedVertices: Map[Int, VType],
                          graph: Graph[VType, EType]): Graph[VType, EType] = {
    var newGraph = graph
    val newEdgesToAdd = newEdgeAdd.iterator
    while (newEdgesToAdd.hasNext) {
      val edge = newEdgesToAdd.next()
      val from = addedVertices(edge._1)
      val to = addedVertices(edge._2)
      val newEdge = edgeFactory.produce(from, to)

      newGraph += newEdge
    }

    newGraph
  }

  private def addOldEdges(oldEdgeAdd: Set[(VType, VType)],
                          morphism: Morphism[VType],
                          graph: Graph[VType, EType]): Graph[VType, EType] = {
    var newGraph = graph
    val oldEdgesToAdd = oldEdgeAdd.iterator
    while (oldEdgesToAdd.hasNext) {
      val edges = oldEdgesToAdd.next()
      val from = morphism.getVertexCorrespondingTo(edges._1)
      val to = morphism.getVertexCorrespondingTo(edges._2)
      newGraph += edgeFactory.produce(from, to)
    }

    newGraph
  }

  private def addMixEdges(mixEdgeAdd: Set[(Int, VType)],
                          addedVertices: Map[Int, VType],
                          morphism: Morphism[VType],
                          graph: Graph[VType, EType]): Graph[VType, EType] = {
    var newGraph = graph
    val mixEdgesToAdd = mixEdgeAdd.iterator
    while(mixEdgesToAdd.hasNext) {
      val edge = mixEdgesToAdd.next()
      val from = addedVertices(edge._1)
      val to = morphism.getVertexCorrespondingTo(edge._2)
      newGraph += edgeFactory.produce(from, to)
    }

    newGraph
  }

  private def removeEdges(oldEdgeRemove: Set[(VType, VType)],
                          morphism: Morphism[VType],
                          totalGraph: Graph[VType, EType],
                          graph: Graph[VType, EType]): Graph[VType, EType] = {
    var newGraph = graph
    val oldEdgesToRemove = oldEdgeRemove.iterator
    while (oldEdgesToRemove.hasNext) {
      val vertexPair = oldEdgesToRemove.next()
      val a = morphism.getVertexCorrespondingTo(vertexPair._1)
      val b = morphism.getVertexCorrespondingTo(vertexPair._2)
      val edge: Option[Graph[VType, EType]#EdgeT] = totalGraph.edges.find(e => Set(e._1, e._2) == Set(a, b))

      if (edge.isDefined)
        newGraph -= edge.get
      else
        throw new IllegalArgumentException("The graph " + totalGraph + " didn't contain the edge " + a + " ~ " + b)
    }

    newGraph
  }

  /**
   * Throws an exception if the production attempts to both add an edge to a previous vertex, as well as remove it.
   */
  private def noOverlappingVerticesBetweenVertexRemovalAndEdgeAdditionAllowed(): Unit = {
    var verticesInvolvedInEdgeRemoval = Set[VType]()
    oldEdgeAdd.foreach(fromTo => {
      verticesInvolvedInEdgeRemoval += fromTo._1
      verticesInvolvedInEdgeRemoval += fromTo._2
    })

    mixEdgeAdd.foreach(newToOld => verticesInvolvedInEdgeRemoval += newToOld._2)

    val overlapping = vertexRemove.intersect(verticesInvolvedInEdgeRemoval)
    if (!overlapping.isEmpty)
      throw new IllegalArgumentException("Vertices " + overlapping.mkString(", ") + " cannot both be removed and involved in edge construction. Edges connected to a removed vertex will also be removed.")
  }

  /**
   * Throws an exception if the production attempts to both add and remove an edge between two old vertices.
   */
  private def addingAndRemovingAnEdgeException() {
    var vertexPairs = Set[(VType, VType)]()
    oldEdgeAdd.foreach(fromTo => {
      vertexPairs += ((fromTo._1, fromTo._2))
      vertexPairs += ((fromTo._2, fromTo._1))
    })

    val edgeRemoval = oldEdgeRemove.iterator
    while (edgeRemoval.hasNext) {
      val pair = edgeRemoval.next()
      if (vertexPairs.contains((pair._1, pair._2)))
        throw new IllegalArgumentException("Attempted to both add and remove an edge between " + pair._1 + " and " + pair._2)
    }
  }

  /**
   * Throws an exception if the production attempts to replace a vertex that's about to be removed.
   */
  private def removingAndReplacingVertexException(): Boolean = {
    val intersection = vertexRemove intersect vertexReplace

    if (!intersection.isEmpty)
      throw new IllegalArgumentException("The following vertices are both set to be removed and replace: " + intersection.mkString(", "))

    true
  }

  private def modificationIdsExistException(): Boolean = {
    if (!newRoomModifications.isEmpty && !newRoomModifications.keySet.subsetOf(vertexAdd))
      throw new Error("Cannot add modifications to ids that haven't been added yet: " + vertexAdd.diff(newRoomModifications.keySet).mkString(", "))

    true
  }
}
